package org.example.project.micro.config;

import org.apache.commons.lang3.StringUtils;
import org.example.project.micro.event.ConfigurationChangeEvent;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.properties.ConfigurationPropertiesBeans;
import org.springframework.cloud.util.ProxyUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ManagedResource
public class ConfigurationPropertiesRebinder2
        implements ApplicationContextAware, ApplicationListener<ConfigurationChangeEvent> {

    private ConfigurationPropertiesBeans beans;

    private ApplicationContext applicationContext;

    private Map<String, Exception> errors = new ConcurrentHashMap<>();

    public ConfigurationPropertiesRebinder2(ConfigurationPropertiesBeans beans) {
        this.beans = beans;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * A map of bean name to errors when instantiating the bean.
     *
     * @return The errors accumulated since the latest destroy.
     */
    public Map<String, Exception> getErrors() {
        return this.errors;
    }

    @ManagedOperation
    public void rebind(Set<String> keys) {
        if (Objects.isNull(applicationContext)) {
            return;
        }

        this.errors.clear();
        for (String name : this.beans.getBeanNames()) {
            Object bean;
            ConfigurationProperties annotation;
            if (!this.beans.getBeanNames().contains(name)
                    || Objects.isNull((bean = this.applicationContext.getBean(name)))
                    || Objects.isNull((annotation = bean.getClass().getAnnotation(ConfigurationProperties.class)))
            ) {
                continue;
            }

            final String prefix = StringUtils.isBlank(annotation.prefix()) ? annotation.value() : annotation.prefix();
            for (String key : keys) {
                if (key.startsWith(prefix)) {
                    rebind(bean, name);
                    break;
                }
            }
        }
    }


    @ManagedOperation
    public void rebind(Object bean, String beanName) {

        try {
            if (AopUtils.isAopProxy(bean)) {
                bean = ProxyUtils.getTargetObject(bean);
            }
            if (bean != null) {
                this.applicationContext.getAutowireCapableBeanFactory()
                        .destroyBean(bean);
                this.applicationContext.getAutowireCapableBeanFactory()
                        .initializeBean(bean, beanName);
            }
        } catch (RuntimeException e) {
            this.errors.put(beanName, e);
            throw e;
        } catch (Exception e) {
            this.errors.put(beanName, e);
            throw new IllegalStateException("Cannot rebind to " + beanName, e);
        }
    }

    @ManagedAttribute
    public Set<String> getBeanNames() {
        return new HashSet<>(this.beans.getBeanNames());
    }

    @Override
    public void onApplicationEvent(ConfigurationChangeEvent event) {
        if (this.applicationContext.equals(event.getSource())
                // Backwards compatible
                || event.getKeys().equals(event.getSource())) {
            rebind(event.getKeys());
        }
    }

}
